package data;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;

/**
 * Low-level data lookup class. Works closely with the data. Can preload the
 * data into memory (takes about 7 hours though) to speed up calculations.
 * 
 * @author Robert Layton
 * @version 0.9 (2008-08-20)
 * 
 */
public class DataLookup {

	static {
		// Load the user IDs;
		DataInfo.getAllUserIDs();
	}

	private int[][] movieIndexedUserIDs;

	private byte[][] movieIndexedRatings;

	private byte[][][] movieIndexedDates;

	private short[][] userIndexedMovieIDs;

	private byte[][] userIndexedRatings;

	private byte[][][] userIndexedDates;

	private double[] userAverages = new double[DataInfo.NUM_USERS];

	private double[] movieAverages = new double[DataInfo.NUM_MOVIES];

	private double[] userStdDev = new double[DataInfo.NUM_USERS];

	private double[] movieStdDev = new double[DataInfo.NUM_MOVIES];

	private boolean getDate = false;

	// Use the probe information?
	private static boolean useProbe = false;

	private DataLookup() {

		// Check we have enough memory to run
		long memory = Runtime.getRuntime().maxMemory() / (1024 * 1024);
		// System.out.println("Memory available: " + memory);
		if (memory < 1000) {
			System.err.println("There is only " + memory
					+ "m amount of memory available.\n"
					+ "This will likely run out of memory soon.");
		}
		loadRatings(getDate);
	}

	public static void useProbe(boolean useProbe) {
		DataLookup.useProbe = useProbe;
		if (dl != null) {
			System.out.println("useProbe changed to " + useProbe
					+ ", so reloading DataLookup now.");
			dl = null;
			System.gc();
			DataLookup.getInstance();
		}
	}

	/**
	 * Loads the ratings to memory. Requires the ratings in user and movie
	 * indexed form already. Does not load date, regardless of what getDate is.
	 * 
	 * @param getDate
	 */
	private final void loadRatings(boolean getDate) {
		try {
			// User-indexed data: Initialise all values
			ObjectInputStream ois = null;
			// Load by movie index, depending on the mode (useDates, useProbe)
			// we want
			String filename = DataInfo.binaryFolder + "movieIndexedSlab.data";
			if (!useProbe && !getDate) {
				filename = DataInfo.binaryFolder
						+ "cleanedMovieIndexedSlab.data";
			} else if (getDate) {
				if (useProbe) {
					filename = DataInfo.binaryFolder
							+ "movieIndexedSlabDates.data";
				} else {
					filename = DataInfo.binaryFolder
							+ "cleanedMovieIndexedSlabDates.data";
				}
			}
			System.out.print("Loading from: " + filename + "...");
			ois = new ObjectInputStream(new FileInputStream(filename));
			movieIndexedUserIDs = (int[][]) ois.readObject();
			movieIndexedRatings = (byte[][]) ois.readObject();
			if (getDate) {
				movieIndexedDates = (byte[][][]) ois.readObject();
			}
			ois.close();
			System.out.println("done");

			// Load by user index
			ois = null;
			filename = DataInfo.binaryFolder + "userIndexedSlab.data";
			if (!useProbe && !getDate) {
				filename = DataInfo.binaryFolder
						+ "cleanedUserIndexedSlab.data";
			} else if (getDate) {
				if (useProbe) {
					filename = DataInfo.binaryFolder
							+ "userIndexedSlabDates.data";
				} else {
					filename = DataInfo.binaryFolder
							+ "cleanedUserIndexedSlabDates.data";
				}
			}
			System.out.print("Loading from: " + filename + "...");
			ois = new ObjectInputStream(new FileInputStream(filename));
			userIndexedMovieIDs = (short[][]) ois.readObject();
			userIndexedRatings = (byte[][]) ois.readObject();
			if (getDate) {
				userIndexedDates = (byte[][][]) ois.readObject();
			}
			ois.close();

		} catch (FileNotFoundException e) {
			throw new RuntimeException("Could not find files: "
					+ e.getLocalizedMessage());
		} catch (IOException e) {
			throw new RuntimeException(
					"Please look at download file. Something is drastically wrong");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		System.out.println("done");
		calcBasicStats();
	}

	private void calcBasicStats() {
		System.out.print("Calculating basic statistics for data...");
		this.userAverages = new double[DataInfo.NUM_USERS];
		for (int userIndex = 0; userIndex < DataInfo.NUM_USERS; userIndex++) {
			byte[] ratings = getUsersRatings(userIndex);
			userAverages[userIndex] = average(ratings);
			userStdDev[userIndex] = stddev(ratings, userAverages[userIndex]);
		}

		for (short movieID = 1; movieID <= DataInfo.NUM_MOVIES; movieID++) {
			byte[] ratings = getMoviesRatings(movieID);
			movieAverages[movieID - 1] = average(ratings);
			movieStdDev[movieID - 1] = stddev(ratings,
					movieAverages[movieID - 1]);
		}

		System.out.println("done");
	}

	private double average(byte[] ratings) {
		if (ratings.length == 0)
			return 2.5; // middle value
		double sum = 0.0;
		for (byte r : ratings)
			if (r >= 1 && r <= 5)
				sum += r;
		return sum / ratings.length;
	}

	private double stddev(byte[] ratings, double mean) {
		if (ratings.length <= 1)
			return 0;
		double sum = 0.0;
		for (byte r : ratings) {
			if (r >= 1 && r <= 5) {
				double e = mean - r;
				sum += e * e;
			}
		}
		double stddev = Math.sqrt(sum / (double) ratings.length);
		return stddev;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see data.DataLookup#getUsersRatings(int, boolean)
	 */
	public final byte[] getUsersRatings(int userIndex) {
		return userIndexedRatings[userIndex];
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see data.DataLookup#getMoviesRatings(short, boolean)
	 */
	public final byte[] getMoviesRatings(short movieID) {
		return movieIndexedRatings[movieID - 1];
	}
	public final byte[] getMoviesRatings(int movieID) {
		return movieIndexedRatings[movieID - 1];
	}
	/*
	 * (non-Javadoc)
	 * 
	 * @see data.DataLookup#getRating(int, short, boolean)
	 */
	public final byte getRating(int userIndex, short movieID)
			throws RecordNotFoundException {
		int l = userIndexedMovieIDs[userIndex].length;
		for (int i = 0; i < l; i++) {
			if (movieID == userIndexedMovieIDs[userIndex][i]) {
				return userIndexedRatings[userIndex][i];
			}
		}
		// Record not found, so throw error
		throw new RecordNotFoundException("Could not find record for user: "
				+ DataInfo.getUsersID(userIndex) + " and movie: " + movieID);
	}

	public static void main(String args[]) {
		DataLookup dl = new DataLookup();

		short movieID = 758;
		System.out.println("Getting all ratings for movie #" + movieID);
		byte[] movieRatings = dl.getMoviesRatings(movieID);
		int[] movieUsers = dl.getMoviesUsers(movieID);
		for (int i = 0; i < movieRatings.length; i++) {
			System.out.println("User: " + movieUsers[i] + ", Rating: "
					+ movieRatings[i]);
		}

		int userID = 2446680;
		System.out.println("Getting all ratings for user #" + userID);
		short[] userMovies = dl.getUsersMovies(userID);
		byte[] userRatings = dl.getUsersRatings(userID);
		for (int i = 0; i < userRatings.length; i++) {
			System.out.println("Movie: " + userMovies[i] + " Rating: "
					+ userRatings[i]);
		}

		System.out.println("Finding rating for user #" + userID
				+ " for movie #" + movieID);
		try {
			System.out.println("Rating: " + dl.getRating(userID, movieID));
		} catch (RecordNotFoundException e) {
			e.printStackTrace();
		}
	}

	// Instance of dl with loading to RAM
	private static DataLookup dl = null;

	public final static DataLookup getInstance() {
		if (dl == null) {
			dl = new DataLookup();
		}
		return dl;

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see data.DataLookup#getUsersMovies(int)
	 */
	public final short[] getUsersMovies(int userIndex) {
		return userIndexedMovieIDs[userIndex];
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see data.DataLookup#getMoviesUsers(short)
	 */
	public final int[] getMoviesUsers(short movieID) {
		return movieIndexedUserIDs[movieID - 1];
	}
	public final int[] getMoviesUsers(int movieID) {
		return movieIndexedUserIDs[movieID - 1];
	}
	public final byte[][] getUsersDates(int userIndex) {
		// []{day, month, year} in second dimension of array
		return userIndexedDates[userIndex];
	}

	public final byte[][] getMoviesDates(short movieID) {
		return movieIndexedDates[movieID - 1];
	}
	public final byte[][] getMoviesDates(int movieID) {
		return movieIndexedDates[movieID - 1];
	}
	public double getUserAverage(int userIndex) {
		return userAverages[userIndex];
	}

	public double getMovieAverage(short movieID) {
		return movieAverages[movieID - 1];
	}
	
	public double getMovieAverage(int movieID){
		return movieAverages[movieID - 1];
	}

	public double getMovieStdDev(short movieID) {
		double s = movieStdDev[movieID - 1];
		if (s == 0.0)
			s = 0.000001;
		return s;
	}
	public double getMovieStdDev(int movieID) {
		double s = movieStdDev[movieID - 1];
		if (s == 0.0)
			s = 0.000001;
		return s;
	}
	public double getUserStdDev(int userIndex) {
		double s = userStdDev[userIndex];
		// So that there is some variance to speak of
		if (s == 0.0)
			s = 0.000001;
		return s;
	}

}
